/*
    libhdvbd - Library for building sophisticated DVB applications
    Copyright (C) 2012 Tony Houghton <h@realh.co.uk>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/* Enhanced tuning data structure and routines to convert to and from strings
 * as used by DVB apps
 */

#include <glib.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "i18n.h"
#include "section.h"
#include "tuning.h"

#define SLOF 11700000
#define LOF1 9750000
#define LOF2 10600000

struct code_rate_table_t {
    fe_code_rate_t v;
    const char *s;
};

static struct code_rate_table_t code_rate_table[] = {
    { FEC_NONE, "NONE" },
    { FEC_1_2, "1/2" },
    { FEC_2_3, "2/3" },
    { FEC_3_4, "3/4" },
    { FEC_4_5, "4/5" },
    { FEC_5_6, "5/6" },
    { FEC_6_7, "6/7" },
    { FEC_7_8, "7/8" },
    { FEC_8_9, "8/9" },
    { FEC_AUTO, "AUTO" },
    { FEC_3_5, "3/5" },
    { FEC_9_10, "9/10" },
    { FEC_AUTO, NULL },
};

struct modulation_table_t {
    fe_modulation_t v;
    const char *s;
};

static struct modulation_table_t modulation_table[] = {
    { QPSK, "QPSK" },
    { QAM_16, "QAM16" },
    { QAM_32, "QAM32" },
    { QAM_64, "QAM64" },
    { QAM_128, "QAM128" },
    { QAM_256, "QAM256" },
    { QAM_AUTO, "AUTO" },
    { VSB_8, "8VSB" },
    { VSB_16, "16VSB" },
    { PSK_8, "8PSK" },
    { APSK_16, "APSK16" },
    { APSK_32, "APSK32" },
    { DQPSK, "DQPSK" },
    { QAM_AUTO, NULL },
};

struct guard_interval_table_t {
    fe_guard_interval_t v;
    const char *s;
};

static struct guard_interval_table_t guard_interval_table[] = {
    { GUARD_INTERVAL_1_32, "1/32" },
    { GUARD_INTERVAL_1_16, "1/16" },
    { GUARD_INTERVAL_1_8, "1/8" },
    { GUARD_INTERVAL_1_4, "1/4" },
    { GUARD_INTERVAL_1_128, "1/128" },
    { GUARD_INTERVAL_19_128, "19/128" },
    { GUARD_INTERVAL_19_256, "19/256" },
    { GUARD_INTERVAL_AUTO, "AUTO" },
    { GUARD_INTERVAL_AUTO, NULL },
};

struct hierarchy_table_t {
    fe_hierarchy_t v;
    const char *s;
};

static struct hierarchy_table_t hierarchy_table[] = {
    { HIERARCHY_NONE, "NONE" },
    { HIERARCHY_1, "1" },
    { HIERARCHY_2, "2" },
    { HIERARCHY_4, "4" },
    { HIERARCHY_AUTO, "AUTO" },
    { HIERARCHY_AUTO, NULL },
};

struct transmission_mode_table_t {
    fe_transmit_mode_t v;
    const char *s;
};

static struct transmission_mode_table_t transmission_mode_table[] = {
    { TRANSMISSION_MODE_2K, "2K"},
    { TRANSMISSION_MODE_8K, "8K"},
    { TRANSMISSION_MODE_4K, "4K"},
    { TRANSMISSION_MODE_1K, "1K"},
    { TRANSMISSION_MODE_16K, "16K"},
    { TRANSMISSION_MODE_32K, "32K"},
    { TRANSMISSION_MODE_AUTO, "AUTO"},
    { TRANSMISSION_MODE_AUTO, NULL}
};

struct roll_off_table_t {
    fe_rolloff_t v;
    const char *s;
};

static struct roll_off_table_t roll_off_table[] = {
    { ROLLOFF_35, "35"},
    { ROLLOFF_20, "20"},
    { ROLLOFF_25, "25"},
    { ROLLOFF_AUTO, "AUTO"},
    { ROLLOFF_AUTO, NULL}
};

static const char *
lookup_prop_name(gconstpointer table_addr, int val)
{
    struct { int val; const char *name; } const * table = table_addr;
    int n;

    for (n = 0; table[n].name != NULL; ++n)
    {
        if (table[n].val == val)
            return table[n].name;
    }
    return "AUTO";
}

/*
static void
set_or_replace_prop(HdvbTuningData *data, guint32 cmd, guint32 val)
{
    int n;

    for (n = 0; n < data->pprops.num; ++n)
    {
        if (data->props[n].cmd == cmd)
            break;
    }
    if (n == data->pprops.num)
    {
        data->props[n].cmd = cmd;
        ++data->pprops.num;
    }
    data->props[n].u.data = val;
}
*/

static guint32
lookup_prop(const HdvbTuningData *data, guint32 cmd)
{
    int n;

    for (n = 0; n < data->pprops.num; ++n)
    {
        if (data->props[n].cmd == cmd)
            return data->props[n].u.data;
    }
    return 0xffffffff;
}

static guint32
parse_number(const char *s)
{
    char *endptr;
    guint32 result = (guint32) strtol(s, &endptr, 10);

    if (endptr == s || *endptr != 0)
        return 0;
    return result;
}

static guint32
parse_bandwidth(const char *s)
{
    int l;
    double result;
    char *end = NULL;

    if (!strcmp(s, "AUTO"))
        return G_MAXUINT32;
    l = strlen(s);
    if (s[l - 3] != 'M' || s[l - 2] != 'H' ||
            (s[l - 1] != 'z' && s[l - 1] != 'Z'))
    {
        return G_MAXUINT32;
    }
    if (!strncmp(s, "1.712", 5))
        return 1712000;
    result = strtod(s, &end);
    if (result == 0 && end == s)
        return G_MAXUINT32;
    return (guint32) (result * 1000000);
}

guint32
parse_code_rate(const char *s)
{
    int n;

    for (n = 0; code_rate_table[n].s; ++n)
    {
        if (!strcmp(code_rate_table[n].s, s))
            return code_rate_table[n].v;
    }
    return G_MAXUINT32;
}

guint32
parse_modulation(const char *s)
{
    int n;

    for (n = 0; modulation_table[n].s; ++n)
    {
        if (!strcmp(modulation_table[n].s, s))
            return modulation_table[n].v;
    }
    return G_MAXUINT32;
}

guint32
parse_transmission_mode(const char *s)
{
    int n;

    for (n = 0; transmission_mode_table[n].s; ++n)
    {
        if (!g_ascii_strcasecmp(transmission_mode_table[n].s, s))
            return transmission_mode_table[n].v;
    }
    return G_MAXUINT32;
}

guint32
parse_guard_interval(const char *s)
{
    int n;

    for (n = 0; guard_interval_table[n].s; ++n)
    {
        if (!strcmp(guard_interval_table[n].s, s))
            return guard_interval_table[n].v;
    }
    return G_MAXUINT32;
}

guint32
parse_hierarchy(const char *s)
{
    int n;

    for (n = 0; hierarchy_table[n].s; ++n)
    {
        if (!strcmp(hierarchy_table[n].s, s))
            return hierarchy_table[n].v;
    }
    return G_MAXUINT32;
}

guint32
parse_roll_off(const char *s)
{
    int n;

    for (n = 0; hierarchy_table[n].s; ++n)
    {
        if (!strcmp(roll_off_table[n].s, s))
            return roll_off_table[n].v;
    }
    return G_MAXUINT32;
}

inline static void
set_prop(HdvbTuningData *data, int index, guint32 cmd, guint32 val)
{
    data->props[index].cmd = cmd;
    data->props[index].u.data = val;
}

static gboolean
parse_t(char **tokens, HdvbTuningData *data)
{
    int n, i, t2;
    gboolean result = TRUE;
    guint32 val;

    data->type = FE_OFDM;
    for (n = 0; tokens[n]; ++n);
    i = 0;
    if (tokens[0][1] == '2')
    {
        t2 = 2;
        if (n > 1)
            set_prop(data, i++, DTV_STREAM_ID, parse_number(tokens[1]));
        /* Ignore system id */
        set_prop(data, i++, DTV_DELIVERY_SYSTEM, SYS_DVBT2);
    }
    else
    {
        t2 = 0;
        set_prop(data, i++, DTV_DELIVERY_SYSTEM, SYS_DVBT);
    }
    if (n < 2 + t2)
        val = 0;
    else
        val = parse_number(tokens[1 + t2]);
    if (!val)
    {
        data->pprops.num = 0;
        return FALSE;
    }
    set_prop(data, i++, DTV_FREQUENCY, val);
    if (n > 2 + t2)
        val = parse_bandwidth(tokens[2 + t2]);
    else
        val = G_MAXUINT32;
    if (val != G_MAXUINT32)
        set_prop(data, i++, DTV_BANDWIDTH_HZ, val);
    else
        result = FALSE;
    if (n > 3 + t2)
        val = parse_code_rate(tokens[3 + t2]);
    else
        val = G_MAXUINT32;
    if (val == G_MAXUINT32)
    {
        val = FEC_AUTO;
        result = FALSE;
    }
    set_prop(data, i++, DTV_CODE_RATE_HP, val);
    if (n > 4 + t2)
        val = parse_code_rate(tokens[4 + t2]);
    else
        val = G_MAXUINT32;
    if (val == G_MAXUINT32)
    {
        val = FEC_AUTO;
        result = FALSE;
    }
    set_prop(data, i++, DTV_CODE_RATE_LP, val);
    if (n > 5 + t2)
        val = parse_modulation(tokens[5 + t2]);
    else
        val = G_MAXUINT32;
    if (val == G_MAXUINT32)
    {
        val = QAM_AUTO;
        result = FALSE;
    }
    set_prop(data, i++, DTV_MODULATION, val);
    if (n > 6 + t2)
        val = parse_transmission_mode(tokens[6 + t2]);
    else
        val = G_MAXUINT32;
    if (val == G_MAXUINT32)
    {
        val = TRANSMISSION_MODE_AUTO;
        result = FALSE;
    }
    set_prop(data, i++, DTV_TRANSMISSION_MODE, val);
    if (n > 7 + t2)
        val = parse_guard_interval(tokens[7 + t2]);
    else
        val = G_MAXUINT32;
    if (val == G_MAXUINT32)
    {
        val = GUARD_INTERVAL_AUTO;
        result = FALSE;
    }
    set_prop(data, i++, DTV_GUARD_INTERVAL, val);
    if (n > 8 + t2)
        val = parse_hierarchy(tokens[8 + t2]);
    else
        val = G_MAXUINT32;
    if (val == G_MAXUINT32)
    {
        val = HIERARCHY_AUTO;
        result = FALSE;
    }
    set_prop(data, i++, DTV_HIERARCHY, val);
    set_prop(data, i++, DTV_INVERSION, INVERSION_AUTO);
    set_prop(data, i++, DTV_TUNE, 0);
    data->pprops.num = i;
    if (!result)
        return FALSE;
    return (n == 9 && t2 == 0) || (n == 11 && t2 == 2);
}

static gboolean
parse_s(char **tokens, HdvbTuningData *data)
{
    int n, i;
    gboolean result = TRUE;
    guint32 val;

    data->type = FE_QPSK;
    for (n = 0; tokens[n]; ++n);
    if (n < 2)
        val = 0;
    else
        val = parse_number(tokens[1]);
    if (!val)
    {
        data->pprops.num = 0;
        return FALSE;
    }
    if (val < SLOF)
    {
        val -= LOF1;
        data->tone = SEC_TONE_OFF;
    }
    else
    {
        val -= LOF2;
        data->tone = SEC_TONE_ON;
    }
    i = 0;
    if (n > 5 && tokens[0][1] != '1')
        set_prop(data, i++, DTV_DELIVERY_SYSTEM, SYS_DVBS2);
    else
        set_prop(data, i++, DTV_DELIVERY_SYSTEM, SYS_DVBS);
    set_prop(data, i++, DTV_FREQUENCY, val);
    if (n > 2)
    {
        if (tokens[2][0] == 'H' || tokens[2][0] == 'L')
        {
            val = SEC_VOLTAGE_18;
        }
        else if (tokens[2][0] == 'V' || tokens[2][0] == 'R')
        {
            val = SEC_VOLTAGE_13;
        }
        else
        {
            val = G_MAXUINT32;
        }
        if (tokens[2][1])
            result = FALSE;
    }
    else
    {
        val = G_MAXUINT32;
    }
    if (val == G_MAXUINT32)
    {
        result = FALSE;
    }
    else
    {
        set_prop(data, i++, DTV_VOLTAGE, val);
    }
    if (n > 3)
        val = parse_number(tokens[3]);
    else
        val = 0;
    if (!val)
    {
        result = FALSE;
    }
    else
    {
        set_prop(data, i++, DTV_SYMBOL_RATE, val);
    }
    if (n > 4)
        val = parse_code_rate(tokens[4]);
    else
        val = G_MAXUINT32;
    if (val == G_MAXUINT32)
    {
        val = FEC_AUTO;
        result = FALSE;
    }
    set_prop(data, i++, DTV_INNER_FEC, val);
    if (n > 5)
    {
        set_prop(data, i++, DTV_PILOT, PILOT_AUTO);
        val = parse_roll_off(tokens[5]);
        if (val == G_MAXUINT32)
        {
            result = FALSE;
            val = ROLLOFF_AUTO;
        }
    }
    else
    {
        val = ROLLOFF_35;
    }
    set_prop(data, i++, DTV_ROLLOFF, val);
    if (n > 6)
    {
        val = parse_modulation(tokens[6]);
        if (val == G_MAXUINT32)
        {
            result = FALSE;
            val = QPSK;
        }
    }
    else
    {
        val = QPSK;
    }
    set_prop(data, i++, DTV_MODULATION, val);
    set_prop(data, i++, DTV_INVERSION, INVERSION_AUTO);
    set_prop(data, i++, DTV_TUNE, 0);
    data->pprops.num = i;
    if (!result)
        return FALSE;
    return (n >= 5 && n <= 7);
}

fe_type_t
hdvb_tuning_char_to_type(char c)
{
    switch (c)
    {
        case 'S':
            return FE_QPSK;
        case 'C':
            return FE_QAM;
        case 'T':
            return FE_OFDM;
        case 'A':
            return FE_ATSC;
    }
    return -1;
}

gboolean
hdvb_tuning_string_to_data(const char *str, HdvbTuningData *data)
{
    gboolean result = FALSE;
    char **tokens = g_strsplit(str, " ", 0);
    int n;
    gboolean comment = FALSE;

    for (n = 0; tokens[n]; ++n)
    {
        if (tokens[n][0] == '#')
        {
            comment = TRUE;
        }
        if (comment)
        {
            g_free(tokens[n]);
            tokens[n] = NULL;
        }
    }
    memset(data->props, 0, sizeof(struct dtv_property) * HDVB_TUNING_MAX_PROPS);
    data->pprops.props = data->props;
    switch (str[0])
    {
        case 'T':
            result = parse_t(tokens, data);
            break;
        case 'S':
            result = parse_s(tokens, data);
            break;
        /*
        case 'C':
            result = parse_c(stokens, data);
            break;
        */
        /* No point in supporting ATSC until prepared to support its SI */
        default:
            data->pprops.num = 0;
            result = FALSE;
    }
    g_strfreev(tokens);
    return result;
}

static char *
terrestrial_tuning_data_to_string(const HdvbTuningData *td)
{
    const char *t;
    char *plp;
    int del_sys = lookup_prop(td, DTV_DELIVERY_SYSTEM);
    char *result;
    int bw = lookup_prop(td, DTV_BANDWIDTH_HZ);
    char *bw_s = (bw == 1712000) ? g_strdup("1.712") :
            g_strdup_printf("%d", bw / 1000000);

    if (del_sys == SYS_DVBT2)
    {
        /* We can't currently read system_id from tuner */
        t = "T2";
        plp = g_strdup_printf(" %d 0", lookup_prop(td, DTV_STREAM_ID));
    }
    else
    {
        t = "T";
        plp = (char *) "";
    }
    result = g_strdup_printf("%s%s %d %sMHz %s %s %s %s %s %s",
            t, plp,
            lookup_prop(td, DTV_FREQUENCY),
            bw_s,
            lookup_prop_name(code_rate_table,
                    lookup_prop(td, DTV_CODE_RATE_HP)),
            lookup_prop_name(code_rate_table,
                    lookup_prop(td, DTV_CODE_RATE_LP)),
            lookup_prop_name(modulation_table,
                    lookup_prop(td, DTV_MODULATION)),
            lookup_prop_name(transmission_mode_table,
                    lookup_prop(td, DTV_TRANSMISSION_MODE)),
            lookup_prop_name(guard_interval_table,
                    lookup_prop(td, DTV_GUARD_INTERVAL)),
            lookup_prop_name(hierarchy_table,
                    lookup_prop(td, DTV_HIERARCHY)));
    g_free(bw_s);
    if (plp[0])
        g_free(plp);
    return result;
}

static char *
satellite_tuning_data_to_string(const HdvbTuningData *td)
{
    const char *s;
    int del_sys = lookup_prop(td, DTV_DELIVERY_SYSTEM);
    char *result;
    int freq = lookup_prop(td, DTV_FREQUENCY);
    int tone = lookup_prop(td, DTV_TONE);
    int voltage = lookup_prop(td, DTV_VOLTAGE);
    char *s2;
    char polar;

    if (del_sys == SYS_DVBS2)
    {
        s = "S2";
        s2 = g_strdup_printf(" %s %s",
                lookup_prop_name(roll_off_table,
                        lookup_prop(td, DTV_ROLLOFF)),
                lookup_prop_name(modulation_table,
                        lookup_prop(td, DTV_MODULATION)));
    }
    else
    {
        s = "S1";
        s2 = (char *) "";
    }
    switch (tone)
    {
        case SEC_TONE_OFF:
            freq += LOF1;
            break;
        case SEC_TONE_ON:
            freq += LOF2;
            break;
        default:
            g_warning(_("Unrecognised DVB-S tone code %d"), tone);
            break;
    }
    switch (voltage)
    {
        /* How are we supposed to tell if it's really L/R? */
        case SEC_VOLTAGE_18:
            polar = 'H';
            break;
        case SEC_VOLTAGE_13:
            polar = 'V';
            break;
        default:
            polar = 'H';
            g_warning(_("Unrecognised DVB-S voltage code %d"), voltage);
            break;
    }
    result = g_strdup_printf("%s %d %c %d %s%s",
            s,
            freq,
            polar,
            lookup_prop(td, DTV_SYMBOL_RATE),
            lookup_prop_name(code_rate_table,
                    lookup_prop(td, DTV_INNER_FEC)),
            s2);
    if (s2[0])
        g_free(s2);
    return result;
}

char *
hdvb_tuning_data_to_string(const HdvbTuningData *td)
{
    switch (td->type)
    {
        case FE_OFDM:
            return terrestrial_tuning_data_to_string(td);
        case FE_QPSK:
            return satellite_tuning_data_to_string(td);
        default:
            g_warning(_("Don't know how to make a string from tuning details "
                    "of type %d"), td->type);
            break;
    }
    return NULL;
}

gboolean
hdvb_tuning_strs_are_similar(const char *t1, const char *t2)
{
    guint f1, f2;
    guint divisor;

    if (!t1)
        return !t2;
    if (t1[0] != t2[0] || t1[1] != t2[1])
        return FALSE;
    if (t1[0] == 'S')
    {
        char p1, p2;
        sscanf(t1 + 3, "%d %c", &f1, &p1);
        sscanf(t2 + 3, "%d %c", &f2, &p2);
        /* AIUI you can have more than one polarity on the same frequency */
        if (p1 != p2)
            return FALSE;
        divisor = 2000;
    }
    else
    {
        if (t1[1] == '2')
        {
            int id1_1, id1_2, id2_1, id2_2;
            sscanf(t1 + 3, "%d %d %d", &id1_1, &id1_2, &f1);
            sscanf(t2 + 3, "%d %d %d", &id2_1, &id2_2, &f2);
            if (id1_1 != id2_1 || id1_2 != id2_2)
                return FALSE;
        }
        else
        {
            sscanf(t1 + 2, "%d", &f1);
            sscanf(t2 + 2, "%d", &f2);
        }
        divisor = 2000000;
    }
    return f1 / divisor == f2 / divisor;
}
